package com.xiangxiao.rpan.authority.oauth;

import com.xiangxiao.rpan.authority.constant.GlobalConsts;
import com.xiangxiao.rpan.authority.exception.LoginException;
import com.xiangxiao.rpan.authority.model.UserInfo;
import com.xiangxiao.rpan.authority.service.impl.UserserviceImpl;
import com.xiangxiao.rpan.authority.vo.UserVo;
import com.xiangxiao.rpan.utils.GeoIpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.common.*;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.core.AuthenticationException;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @auther xiangxiao
 * @email 573768011@qq.com
 * @data 2024/10/16 17:30
 */
/**
 * 自定义的Token生成逻辑：整体的代码并没有太多变动。
 * 主要用来实现同一个账号在多台设备登录的同一个端(Web/APP)时，允许互踢下线或者同时在线的需求
 * 多个端同时登录是根据不同的授权客户端来区分的(表oauth_client_details)，
 * 而同一个端如何区分不同设备是根据登录的机器的ip来区分的，登录着的ip信息会记录在登录信息中
 */
public class RpanTokenService extends DefaultTokenServices {
  private static final Logger logger = LoggerFactory.getLogger(RpanTokenService.class);

  private TokenStore tokenStore;

  private TokenEnhancer accessTokenEnhancer;

  private UserserviceImpl userService;

  private TokenCodec tokenCodec = new TokenCodec();

  @Autowired
  private HttpServletRequest request;

  /**
   * 同一个账号在同一个端是否只能存在一个登录，如果为true，那么同一个账号在多个设备登录web账号时，
   * 后面登录的账号会踢掉前面登录的账号。
   */
  private boolean loginOnlyOneDevice = true;

  public RpanTokenService(TokenStore tokenStore, TokenEnhancer accessTokenEnhancer, UserserviceImpl userService) {
    this.tokenStore = tokenStore;
    this.accessTokenEnhancer = accessTokenEnhancer;
    this.userService = userService;
    super.setTokenStore(tokenStore);
  }

  /**
   * 设置自定义的token信息返回处理器
   * @param tokenEnhancer
   */
  public void setAccessTokenEnhancer(TokenEnhancer tokenEnhancer) {
    this.accessTokenEnhancer = tokenEnhancer;
  }

  /**
   * 是否允许多个机器登录同一个账号，并且不互踢下线
   * @param loginOnlyOneDevice
   */
  public void setLoginOnlyOneDevice(boolean loginOnlyOneDevice) {
    this.loginOnlyOneDevice = loginOnlyOneDevice;
  }

  /**
   * 生成token信息
   *
   * @param authentication
   * @return
   * @throws AuthenticationException
   */
  @Override
  public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
    if (authentication == null) {
      throw new InvalidGrantException("登录信息验证失败");
    }

    try {
      //分布式锁，解决同一客户端同一账号同时登陆的并发问题
      RpanRedisTokenStore redisTokenStore = (RpanRedisTokenStore) tokenStore;
      String key = "login:" + redisTokenStore.getOAuth2AuthenticationKey(authentication);
      OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
      OAuth2RefreshToken refreshToken = null;

      // 更新登录方式和时间
      if (existingAccessToken != null) {
        existingAccessToken.getAdditionalInformation().put(GlobalConsts.METHOD,
            authentication.getOAuth2Request().getRequestParameters().get("grant_type"));
        existingAccessToken.getAdditionalInformation().put(GlobalConsts.LOGIN_TIME, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        existingAccessToken.getAdditionalInformation().put(GlobalConsts.IP, GeoIpUtil.getIpAddress(request));
      }

      if (existingAccessToken != null && loginOnlyOneDevice) {
        //后登录的挤掉前面登录的
        tokenStore.removeAccessToken(existingAccessToken);
        ((RpanRedisTokenStore) tokenStore).getRedisTemplate().opsForValue().getOperations().delete(existingAccessToken.getValue());
        existingAccessToken = null;
        Map<String, String> requestParameters = authentication.getOAuth2Request().getRequestParameters();
        Map<String, String> parametersMap = new HashMap<>();
        if (requestParameters != null) {
          parametersMap.putAll(requestParameters);
        }
        parametersMap.put("message", "您的账号已在其它地方登录，如非本人操作，请尽快修改密码并联系管理员！");
        Field field = OAuth2Request.class.getSuperclass().getDeclaredField("requestParameters");
        field.setAccessible(true);
        field.set(authentication.getOAuth2Request(), parametersMap);
      }

      if (existingAccessToken != null) {
        if (existingAccessToken.isExpired()) {
          if (existingAccessToken.getRefreshToken() != null) {
            refreshToken = existingAccessToken.getRefreshToken();
            // The token store could remove the refresh token when the
            // access token is removed, but we want to
            // be sure...
            tokenStore.removeRefreshToken(refreshToken);
          }
          tokenStore.removeAccessToken(existingAccessToken);
        } else {
          // Re-store the access token in case the authentication has changed
          tokenStore.storeAccessToken(existingAccessToken, authentication);
          return existingAccessToken;
        }
      }

      // Only create a new refresh token if there wasn't an existing one
      // associated with an expired access token.
      // Clients might be holding existing refresh tokens, so we re-use it in
      // the case that the old access token
      // expired.
      if (refreshToken == null) {
        refreshToken = createRefreshToken0(authentication);
      }
      // But the refresh token itself might need to be re-issued if it has
      // expired.
      else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
        ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
        if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
          refreshToken = createRefreshToken0(authentication);
        }
      }

      OAuth2AccessToken accessToken = createAccessToken0(authentication, refreshToken);
      tokenStore.storeAccessToken(accessToken, authentication);
      // In case it was modified
      refreshToken = accessToken.getRefreshToken();
      if (refreshToken != null) {
        tokenStore.storeRefreshToken(refreshToken, authentication);
      }
      return accessToken;
    } catch (Exception e) {
      logger.info("用户登录异常", e);
      return null;
    } finally {
    }
  }

  /**
   * 生成refresh token信息
   *
   * @param authentication
   * @return
   */
  private OAuth2RefreshToken createRefreshToken0(OAuth2Authentication authentication) {
    if (!isSupportRefreshToken(authentication.getOAuth2Request())) {
      return null;
    }
    int validitySeconds = getRefreshTokenValiditySeconds(authentication.getOAuth2Request());
    UserOAuthDetail userOAuthDetail = (UserOAuthDetail) authentication.getPrincipal();
    UserInfo userInfo = userOAuthDetail.getUserInfo();
    UserVo userVo = userInfo.getUserVo();
    OAuth2Request authorizationRequest = authentication.getOAuth2Request();
    String tokenContent = tokenCodec.createToken(0L, userVo.getAccount(), authorizationRequest.getClientId());
    if (validitySeconds > 0) {
      return new DefaultExpiringOAuth2RefreshToken(tokenContent, new Date(System.currentTimeMillis()
          + (validitySeconds * 1000L)));
    }
    return new DefaultOAuth2RefreshToken(tokenContent);
  }

  /**
   * 生成token信息
   *
   * @param authentication
   * @param refreshToken
   * @return
   */
  private OAuth2AccessToken createAccessToken0(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
    UserOAuthDetail userOAuthDetail = (UserOAuthDetail) authentication.getPrincipal();
    UserInfo userInfo = userOAuthDetail.getUserInfo();
    UserVo userVo = userInfo.getUserVo();
    OAuth2Request authorizationRequest = authentication.getOAuth2Request();
    String tokenContent = tokenCodec.createToken(0L, userVo.getAccount(), authorizationRequest.getClientId());
    DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(tokenContent);
    int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
    if (validitySeconds > 0) {
      token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
    }
    token.setRefreshToken(refreshToken);
    token.setScope(authentication.getOAuth2Request().getScope());
    return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
  }
}
